
#include <stdio.h>
#include <string.h>

#include "lg_shader.h"
#include "lg_canvas.h"
#include "lg_material.h"

#define LGOGL_CORE

#include "lgogl_shader.h"
#include "lgogl_texture.h"
#include "lgogl_canvas.h"

typedef struct {
	LGShader shader;
	GLuint program;
	GLint projPos;
	GLint viewPos;
	GLint transformPos;
}LGOGLShader;

typedef struct {
	int location;
}UniformPart;


static const char VERT_HEADER[] = "\n"
"#version 330 core\n"
"layout (location = 0) in vec4 lgPosition;\n"
"layout (location = 1) in vec4 lgColor;\n"
"layout (location = 2) in vec4 lgUv;\n"
"layout (location = 3) in vec4 lgNormal;\n"
"layout (location = 4) in vec4 lgVector0;\n"
"layout (location = 5) in vec4 lgVector1;\n"
"layout (location = 6) in vec4 lgVector2;\n"
"layout (location = 7) in vec4 lgVector3;\n"
"uniform mat4 lgTransform = mat4(1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0);\n"
"uniform mat4 lgView = mat4(1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0);\n"
"uniform mat4 lgProjection = mat4(1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0);\n"
;

static const char FRAG_HEADER[] = "\n"
"#version 330 core\n"
;

#define ERR_LEN 1024
static struct {
	char err[ERR_LEN];
	LGShader* curBind;
	const char* source[LGSHADER_MAX_SOURCE];
	GLint lengths[LGSHADER_MAX_SOURCE];
	LGDepthTest depthTest;
}ctx = {0};


static void
bindDephtTest(LGDepthTest depthTest) {

	//glDisable(GL_DEPTH_TEST);
	//glEnable(GL_DEPTH_TEST);
	//glDepthFunc(GL_ALWAYS);
	return;
	//if (ctx.depthTest == depthTest) {
	//	return;
	//}
	ctx.depthTest = depthTest;
	switch (depthTest) {
	case LG_DEPTH_TEST_ALWAYS:
		glDepthFunc(GL_ALWAYS);
		glEnable(GL_DEPTH_TEST);
		break;
	case LG_DEPTH_TEST_NEVER:
		glDepthFunc(GL_NEVER);
		glEnable(GL_DEPTH_TEST);
		break;
	case LG_DEPTH_TEST_LESS:
		glDepthFunc(GL_LESS);
		glEnable(GL_DEPTH_TEST);
		break;
	case LG_DEPTH_TEST_GREATER:
		glDepthFunc(GL_GREATER);
		glEnable(GL_DEPTH_TEST);
		break;
	case LG_DEPTH_TEST_NEQUAL:
		glDepthFunc(GL_NOTEQUAL);
		glEnable(GL_DEPTH_TEST);
		break;
	case LG_DEPTH_TEST_LEQUAL:
		glDepthFunc(GL_LEQUAL);
		glEnable(GL_DEPTH_TEST);
		break;
	case LG_DEPTH_TEST_GEQUAL:
		glDepthFunc(GL_GEQUAL);
		glEnable(GL_DEPTH_TEST);
		break;
	default:
		glDisable(GL_DEPTH_TEST);
	}
}

static void
setUniformF(GLint pos, LGFloat f) {
	glUniform1f(pos, (GLfloat)f);
}

static void
setUniformV(GLint pos, LGVector* v) {
	glUniform4f(pos, (GLfloat)v->x,
		(GLfloat)v->y, (GLfloat)v->z, (GLfloat)v->w);
}

static void
setUniformM(GLint pos, LGMatrix* mat) {
	GLfloat t[16];
	for (int i = 0; i < 16; i++) {
		t[i] = (GLfloat)mat->B[i];
	}
	glUniformMatrix4fv(pos, 1, GL_FALSE, t);
}

static void
uploadUniforms(LGMaterial* material) {
	bindDephtTest(material->depthTest);
	LGOGLShader* oglShader = (LGOGLShader*)material->shader;
	GLint pos;
	LGUniform* u;
	for (unsigned i = 0; i < material->count; i++) {
		u = &material->uniforms[i];
		pos = glGetUniformLocation(oglShader->program, u->name);
		if (pos < 0) {
			continue;
		}
		switch (u->type) {
		case LG_TFLOAT:
			setUniformF(pos, u->f);
			break;
		case LG_TVECTOR:
			setUniformV(pos, &u->v);
			break;
		case LG_TMATRIX:
			setUniformM(pos, &u->m);
			break;
		case LG_TTEXTURE:
			glActiveTexture(GL_TEXTURE0 + pos);
			glBindTexture(GL_TEXTURE_2D, lgoglGetTextureId(u->tex));
			GLint err = glGetError();
			if (err) {
				printf("LG_TTEXTURE err:%d\n", err);
			}
			break;
		case LG_TCANVAS:
			glActiveTexture(GL_TEXTURE0 + pos);
			glBindTexture(GL_TEXTURE_2D, lgoglGetCanvasTexId(u->canvas));
			break;
		}
	}
}

static size_t
sizeofShader(){
	return sizeof(LGOGLShader);
}

static inline LGOGLShader*
bindShader(LGShader* shader) {
	if (ctx.curBind == shader) {
		return (LGOGLShader*)shader;
	}
	LGOGLShader* oglShader = (LGOGLShader*)shader;
	if (!oglShader->program) {
		return NULL;
	}
	ctx.curBind = shader;
	glUseProgram(oglShader->program);
	return oglShader;
}

static int
newShader(LGShader* shader) {
	LGOGLShader* oglShader = (LGOGLShader*)shader;
	oglShader->program = 0;
	return 0;
}
static void
freeShader(LGShader* shader) {
	LGOGLShader* oglShader = (LGOGLShader*)shader;
	if (oglShader->program) {
		glDeleteProgram(oglShader->program);
	}
	if (ctx.curBind == shader){
		ctx.curBind = NULL;
		glUseProgram(0);
	}
}

static int
compileShader(LGShader* shader, const char** err) {
	LGOGLShader* oglShader = (LGOGLShader*)shader;
	GLint ret;
	const char** source = ctx.source;
	GLint* lengths = ctx.lengths;
	source[0] = VERT_HEADER;
	lengths[0] = (GLint)strlen(VERT_HEADER);
	for(unsigned i = 0; i < shader->vsCount; i ++){
		source[i + 1] = shader->vs[i].data;
		lengths[i + 1] = (GLint)shader->vs[i].size;
	}
	GLuint vsId= glCreateShader(GL_VERTEX_SHADER);
	glShaderSource(vsId, shader->vsCount + 1, source, lengths);
	glCompileShader(vsId);
	glGetShaderiv(vsId, GL_COMPILE_STATUS, &ret);
	if (!ret) {
		glGetShaderInfoLog(vsId, ERR_LEN, NULL, ctx.err);
		glDeleteShader(vsId);
		*err = ctx.err;
		return 1;
	}
	source[0] = FRAG_HEADER;
	lengths[0] = (GLint)strlen(FRAG_HEADER);
	for (unsigned i = 0; i < shader->fsCount; i++) {
		source[i + 1] = shader->fs[i].data;
		lengths[i + 1] = (GLint)shader->fs[i].size;
	}
	GLuint fsId = glCreateShader(GL_FRAGMENT_SHADER);
	glShaderSource(fsId, shader->fsCount + 1, source, lengths);
	glCompileShader(fsId);
	glGetShaderiv(fsId, GL_COMPILE_STATUS, &ret);
	if (!ret) {
		glGetShaderInfoLog(fsId, ERR_LEN, NULL, ctx.err);
		glDeleteShader(fsId);
		glDeleteShader(vsId);
		*err = ctx.err;
		return 1;
	}
	GLuint program = glCreateProgram();
	glAttachShader(program, vsId);
	glAttachShader(program, fsId);
	glLinkProgram(program);
	glGetProgramiv(program, GL_LINK_STATUS, &ret);
	glDeleteShader(vsId);
	glDeleteShader(fsId);
	if (!ret) {
		glGetProgramInfoLog(oglShader->program, ERR_LEN, NULL, ctx.err);
		*err = ctx.err;
		glDeleteProgram(oglShader->program);
		return 1;
	}
	oglShader->program = program;
	oglShader->projPos = glGetUniformLocation(program, "lgProjection");
	oglShader->viewPos = glGetUniformLocation(program, "lgView");
	oglShader->transformPos = glGetUniformLocation(program, "lgTransform");
	glUseProgram(program);
	return 0;
}


static LGShaderBackend backend = {
	sizeofShader,
	newShader,
	freeShader,
	compileShader,
};

LGOGL_API LGShaderBackend*
lgoglShaderBackend() {
	return &backend;
}

LGOGL_API void
lgoglBindMaterial(LGMaterial* material) {
	bindShader(material->shader);
	uploadUniforms(material);
}


LGOGL_API void
lgoglBindShaderDraw(LGCanvas* canvas, LGDraw* draw) {
	LGMaterial* material = draw->material;
	LGShader* shader = material->shader;
	LGOGLShader* oglShader = bindShader(shader);
	if (oglShader->projPos >= 0) {
		setUniformM(oglShader->projPos, &canvas->projection);
	}
	if (oglShader->viewPos >= 0) {
		setUniformM(oglShader->viewPos, &canvas->view);
	}
	if (oglShader->transformPos >= 0) {
		setUniformM(oglShader->transformPos, &draw->transform);
	}
	uploadUniforms(material);
}